/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

 package org.apache.ranger.authentication;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.security.KeyStore;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Properties;
import java.util.Set;

import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLServerSocket;
import javax.net.ssl.SSLServerSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import org.apache.hadoop.security.alias.CredentialProvider;
import org.apache.log4j.Logger;
import org.apache.ranger.credentialapi.CredentialReader;
import org.apache.ranger.usergroupsync.UserGroupSync;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

public class UnixAuthenticationService {

	private static final Logger LOG = Logger.getLogger(UnixAuthenticationService.class) ;
	
	private static final String serviceName = "UnixAuthenticationService" ;
	
	private static final String SSL_ALGORITHM = "TLS" ;
	private static final String REMOTE_LOGIN_AUTH_SERVICE_PORT_PARAM = "ranger.usersync.port" ;
	
	private static final String SSL_KEYSTORE_PATH_PARAM = "ranger.usersync.keystore.file" ;
	private static final String SSL_TRUSTSTORE_PATH_PARAM = "ranger.usersync.truststore.file" ;
	
	private static final String SSL_KEYSTORE_PATH_PASSWORD_ALIAS = "usersync.ssl.key.password" ;
	private static final String SSL_TRUSTSTORE_PATH_PASSWORD_ALIAS = "usersync.ssl.truststore.password" ;

	private static final String CRED_VALIDATOR_PROG = "ranger.usersync.passwordvalidator.path" ;
	private static final String ADMIN_USER_LIST_PARAM = "admin.users" ;
	private static final String ADMIN_ROLE_LIST_PARAM = "admin.roleNames" ;
	private static final String SSL_ENABLED_PARAM = "ranger.usersync.ssl" ;
	
	private static final String CREDSTORE_FILENAME_PARAM = "ranger.usersync.credstore.filename" ;
	
	private String keyStorePath ;
	private String keyStorePathPassword ;
	private String trustStorePath ;
	private String trustStorePathPassword ;
	private List<String>  adminUserList = new ArrayList<String>() ;
	private String adminRoleNames ;
	
	private int  portNum ;
	
	private boolean SSLEnabled = false ;
	
	static private boolean enableUnixAuth = false;
	
	private static final String[] UGSYNC_CONFIG_XML_FILES = { "ranger-ugsync-default.xml",  "ranger-ugsync-site.xml" } ; 
	private static final String    PROPERTY_ELEMENT_TAGNAME = "property" ;
	private static final String    NAME_ELEMENT_TAGNAME = "name" ;
	private static final String    VALUE_ELEMENT_TAGNAME = "value" ;

	public static void main(String[] args) {
		if (args.length > 0) {
			for (String s : args) {
				if ("-enableUnixAuth".equalsIgnoreCase(s)) {
					enableUnixAuth = true;
					break;
				}
			}
		}
		UnixAuthenticationService service = new UnixAuthenticationService() ;
		service.run() ;
	}

	public UnixAuthenticationService() {
	}

	
	public void run() {
		try {
			LOG.info("Starting User Sync Service!");
			startUnixUserGroupSyncProcess() ;
			if (enableUnixAuth) {
				LOG.info("Enabling Unix Auth Service!");
			    init() ;
			    startService() ;
			} else {
				LOG.info("Unix Auth Service Disabled!");
			}
		}
		catch(Throwable t) {
			LOG.error("ERROR: Service: " + serviceName , t);
		}
		finally {
			LOG.info("Service: " + serviceName + " - STOPPED.");
		}
	}
	
	private void startUnixUserGroupSyncProcess() {
		//
		//  Start the synchronization service ...
		//
		UserGroupSync syncProc = new UserGroupSync() ;
		Thread newSyncProcThread = new Thread(syncProc) ;
		newSyncProcThread.setName("UnixUserSyncThread");
		newSyncProcThread.setDaemon(false);
		newSyncProcThread.start(); 
	}
	

	//TODO: add more validation code
	private void init() throws Throwable {
		Properties prop = new Properties() ;
		
		for (String fn : UGSYNC_CONFIG_XML_FILES ) {
		
			InputStream in = getFileInputStream(fn) ;
	
			if (in != null) {
				try {
					DocumentBuilderFactory xmlDocumentBuilderFactory = DocumentBuilderFactory.newInstance();
					xmlDocumentBuilderFactory.setIgnoringComments(true);
					xmlDocumentBuilderFactory.setNamespaceAware(true);
					DocumentBuilder xmlDocumentBuilder = xmlDocumentBuilderFactory.newDocumentBuilder();
					Document xmlDocument = xmlDocumentBuilder.parse(in);
					xmlDocument.getDocumentElement().normalize();
	
					NodeList nList = xmlDocument.getElementsByTagName(PROPERTY_ELEMENT_TAGNAME);
	
					for (int temp = 0; temp < nList.getLength(); temp++) {
	
						Node nNode = nList.item(temp);
	
						if (nNode.getNodeType() == Node.ELEMENT_NODE) {
	
							Element eElement = (Element) nNode;
	
							String propertyName = "";
							String propertyValue = "";
							if (eElement.getElementsByTagName(NAME_ELEMENT_TAGNAME).item(
									0) != null) {
								propertyName = eElement
										.getElementsByTagName(NAME_ELEMENT_TAGNAME)
										.item(0).getTextContent().trim();
							}
							if (eElement.getElementsByTagName(VALUE_ELEMENT_TAGNAME)
									.item(0) != null) {
								propertyValue = eElement
										.getElementsByTagName(VALUE_ELEMENT_TAGNAME)
										.item(0).getTextContent().trim();
							}
	
							//LOG.info("Adding Property:[" + propertyName + "] Value:["+ propertyValue + "]");
							if (prop.get(propertyName) != null ) {
								prop.remove(propertyName) ;
	 						}
							prop.put(propertyName, propertyValue);
						}
					}
				}
				finally {
					try {
						in.close();
					}
					catch(IOException ioe) {
						// Ignore IOE when closing streams
					}
				}
			}
		}
		
		String credStoreFileName = prop.getProperty(CREDSTORE_FILENAME_PARAM) ;
		
		keyStorePath = prop.getProperty(SSL_KEYSTORE_PATH_PARAM) ;
		
		if (credStoreFileName == null) {
			throw new RuntimeException("Credential file is not defined. param = [" + CREDSTORE_FILENAME_PARAM + "]") ;
		}
		
		File credFile = new File(credStoreFileName) ;
		
		if (! credFile.exists()) {
			throw new RuntimeException("Credential file [" + credStoreFileName + "]: does not exists." );
		}
		
		if ( ! credFile.canRead() ) {
			throw new RuntimeException("Credential file [" + credStoreFileName + "]: can not be read." );
		}
		
		keyStorePathPassword = CredentialReader.getDecryptedString(credStoreFileName, SSL_KEYSTORE_PATH_PASSWORD_ALIAS) ;
		trustStorePathPassword = CredentialReader.getDecryptedString(credStoreFileName,SSL_TRUSTSTORE_PATH_PASSWORD_ALIAS) ;
		
		trustStorePath  = prop.getProperty(SSL_TRUSTSTORE_PATH_PARAM) ;
		portNum = Integer.parseInt(prop.getProperty(REMOTE_LOGIN_AUTH_SERVICE_PORT_PARAM)) ;
		String validatorProg = prop.getProperty(CRED_VALIDATOR_PROG) ;
		if (validatorProg != null) {
			PasswordValidator.setValidatorProgram(validatorProg);
		}
		
		String adminUsers = prop.getProperty(ADMIN_USER_LIST_PARAM) ; 
		
		if (adminUsers != null && adminUsers.trim().length() > 0) {
			for(String u : adminUsers.split(",")) {
				LOG.info("Adding Admin User:"  + u.trim());
				adminUserList.add(u.trim()) ;
			}
			PasswordValidator.setAdminUserList(adminUserList);
		}
		
		
		adminRoleNames = prop.getProperty(ADMIN_ROLE_LIST_PARAM) ;
		
		if (adminRoleNames != null) {
			LOG.info("Adding Admin Group:" + adminRoleNames);
			PasswordValidator.setAdminRoleNames(adminRoleNames) ;
		}
		
		String SSLEnabledProp = prop.getProperty(SSL_ENABLED_PARAM) ;
		
		SSLEnabled = (SSLEnabledProp != null &&  (SSLEnabledProp.equalsIgnoreCase("true"))) ;
		
//		LOG.info("Key:" + keyStorePath);
//		LOG.info("KeyPassword:" + keyStorePathPassword);
//		LOG.info("TrustStore:" + trustStorePath);
//		LOG.info("TrustStorePassword:" + trustStorePathPassword);
//		LOG.info("PortNum:" + portNum);
//		LOG.info("ValidatorProg:" + validatorProg);
		
	}
	
	
	public void startService() throws Throwable {
		
		SSLContext context =  SSLContext.getInstance(SSL_ALGORITHM) ;
		
		KeyManager[] km = null ;

		if (keyStorePath != null && ! keyStorePath.isEmpty()) {
			KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType()) ;
			
			InputStream in = null ;
			
			in = getFileInputStream(keyStorePath) ;
			
			try {
				if (keyStorePathPassword == null) {
					keyStorePathPassword  = "" ;
				}
				ks.load(in, keyStorePathPassword.toCharArray());
			}
			finally {
				if (in != null) {
					in.close(); 
				}
			}
			
			KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()) ;
			kmf.init(ks, keyStorePathPassword.toCharArray());
			km = kmf.getKeyManagers() ;
		}
		
		
		TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());  
		
		KeyStore trustStoreKeyStore = null ;
		
		if (trustStorePath != null && ! trustStorePath.isEmpty()) {
			trustStoreKeyStore = KeyStore.getInstance(KeyStore.getDefaultType()) ;
			
			InputStream in = null ;
			
			in = getFileInputStream(trustStorePath) ;
			
			try {
				if (trustStorePathPassword == null) {
					trustStorePathPassword = "" ;
				}
				trustStoreKeyStore.load(in, trustStorePathPassword.toCharArray());
			}
			finally {
				if (in != null) {
					in.close(); 
				}
			}
		}
		
		trustManagerFactory.init(trustStoreKeyStore);  
		
		TrustManager[] tm = trustManagerFactory.getTrustManagers() ;
				
		SecureRandom random = new SecureRandom() ;
		
		context.init(km, tm, random);
		
		SSLServerSocketFactory sf = context.getServerSocketFactory() ; 

		ServerSocket socket = (SSLEnabled ? sf.createServerSocket(portNum) :  new ServerSocket(portNum) ) ;
		
		if (SSLEnabled) {
			SSLServerSocket secureSocket = (SSLServerSocket) socket ;
			String[] protocols = secureSocket.getEnabledProtocols() ;
			Set<String> allowedProtocols = new HashSet<String>() ;
			for(String ep : protocols) {
				if (! ep.toUpperCase().startsWith("SSLV3")) {
					LOG.info("Enabling Protocol: [" + ep + "]");
					allowedProtocols.add(ep) ;
				}
				else {
					LOG.info("Disabling Protocol: [" + ep + "]");
				}
			}
			
			if (!allowedProtocols.isEmpty()) {
				secureSocket.setEnabledProtocols(allowedProtocols.toArray(new String[0]));
			}
		}
		
				
		Socket client = null ;
		
		while ( (client = socket.accept()) != null ) {
			Thread clientValidatorThread = new Thread(new PasswordValidator(client)) ;
			clientValidatorThread.start(); 
		}

	}
	
	private InputStream getFileInputStream(String path) throws FileNotFoundException {
		
		InputStream ret = null;
		
		File f = new File(path) ;
		
		if (f.exists()) {
			ret = new FileInputStream(f) ;
		}
		else {
			ret = getClass().getResourceAsStream(path) ;
			if (ret == null) {
				ret = getClass().getResourceAsStream("/" + path) ;
			}
		}
		
		return ret ;
	}

}
